feat: rebuild in-app notifications as contained cards, round-square bot avatar - #29
Merged
Merged
Conversation
…ot avatar Ports the JavaScript SDK's notification redesign (GleapSDK/JavaScript-SDK#138) to the native Android overlay: - Avatar moves inside the card (no speech-bubble tail); sender + relative time render as a meta line below the 2-line-clamped message. - The widget theme drives the look: background, contrast and muted text colors derive from the flow config's backgroundColor (YIQ + shade, same math as the web widget), the card radius from its new borderRadius field (GleapNotificationStyle centralizes the math). - Bots get a rounded-square avatar via sender.isBot (Server#1373); absent flags fall through to the teammate circle. - Multiple notifications collapse into a stack — newest in front, up to two peeks behind, deeper cards hidden; the first tap expands, re-renders collapse. Cards are bottom-anchored in a FrameLayout and placed purely via translationY/scale, with clipBounds trimming taller cards behind the front card's bottom edge. - The close button floats over the stack's top corner (elevation keeps it above the cards; the container chain stops clipping so the overhang and shadows survive). The container branches that sat flush against the screen edge gain the 20dp inset the per-card margins used to provide. - Cap rises from 2 to 4 kept notifications. - Timestamps read sendAt (not createdAt) through ICU's RelativeDateTimeFormatter, localized by the widget language, 'now' under a minute, clamped for future timestamps, dropped entirely below API 24. - Fixes a latent ConcurrentModificationException in addNotification: the news/checklist dedup iterated the message list while removeNotificationViewFromLayout mutated it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A clipped back card (e.g. a news cover) ended exactly at the front card's bottom edge, so its white body showed through the transparent notches behind the front card's rounded bottom corners. Clip to the front card's height in card space instead — after the peek offset and scale-back the clipped bottom lands above the front card's bottom, matching the web widget's clip math. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o button state Three refinements from live review: - Shadows: card elevation drops 6dp -> 4dp and the close button 8dp -> 6dp, with the spot shadow tinted to ~60% black on P+ — soft and airy like the web widget's two-layer shadow instead of the heavier stock elevation look. - Arrivals are choreographed as one deck motion: the previous cards animate back into their tuck while the new card emerges scaled-back from the stack's front slot, instead of the old front snapping into place and the new card floating up from the empty space below the stack. The very first notification keeps the web-style slide-up fade, and re-adds after an overlay rebuild (activity change, reconnect) no longer replay the entrance at all. - The container's constraints re-apply when the feedback button's state flips (setShowFab): the button settles asynchronously after the config loads, and a container built in that window was anchored to the bottom of the screen — a notification arriving then rendered below the button until the next rebuild relocated it, which read as the card animating up from the screen edge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The arrival jank root cause: the stack frame was resized to the stack's expanded height on every change. Cards are bottom-anchored, so the resize re-based every card (and moved the container's top edge) while the arrival animators were mid-flight — the whole deck rendered offset by the height delta and visibly slid across the screen into place, reading as the new card flying in from the bottom of the display. The frame now keeps one fixed height (screen height, floor 1200dp), set once at creation, so nothing ever re-bases: only the card animators move cards. The container carries the same height explicitly in its constraints — left at WRAP_CONTENT, ConstraintLayout measures it AT_MOST the parent and the taller frame overflows past the container's bottom, pushing cards off screen. The close button anchors to the stack's visual top computed from the fixed height, and a newly added card is alpha-0 from the moment it enters the hierarchy so it cannot flash at its resting position before the entrance starts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DecelerateInterpolator(2f) front-loads ~60% of the travel into the first frames — on a long tuck (a tall news card taking the front from a short message) the deck appeared to teleport and then drift. All stack motions (arrival choreography, expand, entrance, close button) now share the material standard-easing curve (0.4, 0, 0.2, 1) at 350ms, matching the gentle read of the iOS ease-out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two structural guarantees replace the per-animation fixes: - The stack frame clips at its bottom edge permanently (generous side/top overscan for shadows and the close-button overhang, a small bottom allowance for the front card's shadow). No card, shadow or mid-animation overhang can ever draw below the stack — the feedback button underneath is untouchable by construction. Previously a tall card's unclipped body slid across the button during tuck/expand transitions, because the per-card clips only applied after the motion settled. - Entrances materialize in place: the new front card (and the very first notification) fades in with a slight scale-up and zero translation, so an arrival can never read as a card traveling in from elsewhere on screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The container-level clip guaranteed containment but guillotined the front card's soft shadow at a straight line, and mid-animation a tall card's body was visible (then bluntly cut) in the strip below the front card. The web widget's model, ported properly: every card carries a clip at ALL times — generous insets past the body in resting states (a visual no-op: Android elevation shadows are outline-based and ignore clipBounds, so nothing is ever cut), the front card's height for collapsed cards behind — and every transition ANIMATES the clip in lockstep with the card's motion (same duration, same curve, starting edge clamped to the card's body). The sweeping edge mathematically never falls below the front card's bottom, so a tall card can neither poke out below the stack mid-flight nor show a cut edge: the clip line always hides behind the front card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Elevation shadows are drawn from the view's outline and ignore the view's alpha — a card (or the close button) fading in cast its shadow at full strength from the first frame, a short dark flicker under a still-invisible element. The card's elevation and the close button's now ramp from zero in sync with their fades. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Elevation 4dp -> 3dp with a lighter spot tint (argb 115) — the barely-there soft throw the iOS two-layer shadow has. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-syncs with the iOS SDK (which shipped 16.4.3 solo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports the JavaScript SDK's notification redesign (GleapSDK/JavaScript-SDK#138) to the native Android overlay, so the three platforms render the same notification system.
Layout
Leonie • 5 minutes ago— the sender half truncates (its max width is computed against the measured time label), the timestamp never does, and the separator only renders when both halves exist.styledCardbuilder: full stack width, the project's container radius, CardView elevation for the shadow, a hairline stroke foreground (API 23+), andclipToOutlineso the news cover squares off against the rounded top corners.Widget-theme colors
The cards were hardcoded white/black. They now derive everything from the flow config, exactly like the web widget's
injectStyledCSS: background frombackgroundColor, contrast by YIQ (≥160 → dark text), muted text by shading the background (+100 dark / −120 light), radius from the newly-parsedborderRadius(× 0.8).GleapNotificationStylecentralizes the math. A dark-themed project now gets dark cards.Bot avatar
A teammate stays circular; the bot gets a rounded square (
max(2, round(round(borderRadius·0.4)·size/36))→ 7dp at the default 20 on the 32dp avatar) through aViewOutlineProviderclip, driven by the newsender.isBot(GleapSDK/Server#1373). Payloads without the flag fall through to the circle, so this ships safely ahead of/behind the server.Notification stack
More than one notification collapses into a stack: newest card in front, up to two older ones peeking above its top edge (9dp / 17dp, scaled 0.955 / 0.91, pivot at top-center), anything deeper hidden. The first tap expands it; any re-render collapses it again. Cards are bottom-anchored in a
FrameLayoutand placed purely viatranslationY/scale; the frame keeps the expanded height and is not clickable, so the empty area above a collapsed stack stays transparent to touches. A taller card behind (e.g. a news cover) is clipped to the front card's height withclipBounds(negative insets spare the shadow), so after peek and scale-back its bottom stays clear of the front card's rounded corners. The clip-chain (clipChildren/clipToPadding) is opened so peeks, shadows and the close-button overhang survive.The cap rises from 2 to 4 kept notifications, and the container branches that sat flush against the screen edge gain the 20dp inset the old per-card margins used to provide.
Close button
Floats over the stack's top-right corner (−9dp overhang, mirrored in RTL via
Gravity.END), on the widget background color with the contrast-tinted cross, elevated above the cards, riding along the stack's visual top as it expands/collapses.Timestamps
ICU
RelativeDateTimeFormatter(API 24+; the label is dropped below), localized off the widget language with a device-locale fallback. ReadssendAtrather thancreatedAt(a scheduled outbound is created long before delivery), collapses anything under a minute to "now", clamps future timestamps, and promotes units (90 minutes → "1 hour").Fixes found along the way
ConcurrentModificationExceptioninaddNotification: the news/checklist dedup iterated the message list whileremoveNotificationViewFromLayoutmutated it (and then callediterator.remove()on top). Reproduced live when a real outbound news card met an injected one; the loop now collects first, removes after.Testing
:gleap:compileDebugJavaWithJavacand:gleap:testDebugUnitTestgreen; demo app built and driven live on the API 36 emulator with injected payloads through the realaddNotificationpath (plus a genuine production outbound news card that arrived alongside): single card, 4-card collapsed stack, tap-to-expand, card-tap opens the conversation and clears, bot rounded-square vs teammate circle, sender truncation, "now"/"5 minutes ago" labels. (Screenshots from the emulator run are with Lukas.)🤖 Generated with Claude Code
Animation architecture (final, after live review)